feat(workstation): escalating shell gate + terminal watch TUI with approve/deny hotkeys - #679
feat(workstation): escalating shell gate + terminal watch TUI with approve/deny hotkeys#679peycheff-com wants to merge 24 commits into
Conversation
|
Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here. |
There was a problem hiding this comment.
Pull request overview
Adds terminal-native operator ergonomics to the kernel CLI by introducing (1) a workstation “escalating shell gate” that can turn blocked commands into pending approvals in the dev profile, and (2) a watch Bubble Tea TUI for live approval queue review with approve/deny hotkeys.
Changes:
- Implement shell command-name extraction + gating decisions, plus a user-editable allowlist store with seeding and mtime/size caching.
- Add
helm-ai-kernel workstation gateto evaluate commands (and optionally create approval ceremonies on the server). - Add
helm-ai-kernel watch(TUI + snapshot modes) and an HTTP client for the approval API, with unit tests.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| core/pkg/workstation/shellgate.go | New shell command parsing/extraction and gating logic (allow/deny/pending_approval). |
| core/pkg/workstation/shellgate_test.go | Unit tests for extraction, allowlist blocking, and gate profile behaviors. |
| core/pkg/workstation/shellallowlist.go | JSON allowlist store (seed defaults, parse multiple formats, cache by mtime+size). |
| core/go.mod | Adds Bubble Tea as a direct dependency (plus new indirect deps). |
| core/go.sum | Adds checksums for Bubble Tea and its transitive dependencies. |
| core/cmd/helm-ai-kernel/workstation_gate_cmd.go | New workstation gate CLI subcommand + optional approval-ceremony creation. |
| core/cmd/helm-ai-kernel/workstation_gate_cmd_test.go | Tests for gate command exit codes, fail-closed behavior, and ceremony creation. |
| core/cmd/helm-ai-kernel/workstation_cmd.go | Wires workstation gate into the workstation command dispatcher/usage. |
| core/cmd/helm-ai-kernel/watch_model.go | Bubble Tea model for approval watching, including fail-closed behavior on refresh errors. |
| core/cmd/helm-ai-kernel/watch_model_test.go | Model tests for filtering/sorting, guards, transitions, rendering, and fail-closed behavior. |
| core/cmd/helm-ai-kernel/watch_cmd.go | New watch command (TUI vs snapshot modes; API key resolution; polling interval). |
| core/cmd/helm-ai-kernel/watch_client.go | HTTP client for approval list/transition/create operations (fail closed on errors). |
| core/cmd/helm-ai-kernel/watch_client_test.go | HTTP client tests (auth header, 401, transitions, bad URL, missing key). |
Comments suppressed due to low confidence (2)
core/pkg/workstation/shellgate.go:124
- sanitizeCommandToken only trims quotes today, so tokens like "pwd)/x" (from
$(pwd)/x) remain intact and get treated as command names. After avoiding splitting on)this still needs to truncate at the first)and trim leading(/ trailing)so command substitutions and subshell parens don’t pollute the extracted command set.
func sanitizeCommandToken(token string) string {
return strings.ToLower(strings.Trim(strings.TrimSpace(token), `'"`))
}
core/pkg/workstation/shellgate.go:108
- unwrapWrappedCommands skips wrapper flags but not their separate values. For wrappers like
env -u PATH rmorsudo -u root rm, the value token (e.g.PATH/root) is currently treated as the wrapped command and the real command (rm) is never extracted. If that value happens to be allowlisted (e.g.env -u ls rm), the gate can become fail-open and miss a blocked command.
if strings.HasPrefix(token, "-") {
// Bare wrapper flag (e.g. `sudo -E`, `time -p`). Flags that take a
// separate value are not unwrapped; the value may surface as a
// command name, which fails closed.
continue
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Port Rowboat's extractCommandNames/isBlocked semantics (Apache-2.0,
apps/cli/src/application/lib/command-executor.ts) to Go as the parsing
front-end of workstation enforcement, with fail-closed hardening:
- ExtractCommandNames: robust to &&, ||, |, ;, &, backticks, $(...)
subshells, ENV= prefixes, and sudo/env/time/command wrappers
(recursive unwrap, skip ENV= and flags after wrappers).
- BlockedCommandNames: empty allowlist blocks everything, '*' allows all.
- GateShellCommand: blocked commands escalate to PENDING APPROVAL in the
dev profile; production (and any unknown profile) stays fail-closed deny.
- ShellAllowlistStore: user-editable JSON allowlist (bare array /
{allowedCommands} / truthy map) with mtime+size cache; seeds the
Rowboat default set on first use; corrupt files fail closed (no
silent fallback to defaults).
Tests: full extraction table (28 cases), blocked semantics, profile
matrix, mtime reload (same-mtime+size cached, bumped-mtime reload),
fail-closed store errors, and the dev escalation round-trip.
Signed-off-by: Mindburn Labs <mindburnlabs@users.noreply.github.com>
(a) helm-ai-kernel watch — terminal-native live approval watcher
(bubbletea TUI, adapted from Rowboat's Ink ui.tsx, Apache-2.0):
- Pending items always derive from server state (GET /api/v1/approvals);
a failed refresh clears the list and disables actions (fail-closed).
- a/d approve/deny hotkeys POST /api/v1/approvals/{id}/{approve,deny}
with the operator actor; r refresh, arrows/jk navigate, q quit.
- --once/--json snapshot modes; automatic snapshot fallback on non-TTY.
- Auth via HELM_ADMIN_API_KEY env or --api-key-file (0600); no argv
secrets. URL via --url or HELM_KERNEL_URL (default 127.0.0.1:8080).
(b) helm-ai-kernel workstation gate — CLI front-end for the escalating
shell gate: --profile dev|production (unknown profiles fail closed to
production), --allowlist path, exit codes 0/3/126 for
allow/pending_approval/deny, and --request-approval which turns a dev
escalation into a real pending approval ceremony on the server (drainable
from watch).
New dep: github.com/charmbracelet/bubbletea v1.3.10 (only direct
addition; no TUI framework existed in go.mod). Rendering is plain-text
to keep the transitive footprint minimal.
Tests: httptest client coverage (auth header, 401, transition paths,
fail-closed without key), TUI model unit tests (pending filter/sort,
fail-closed actions on fetch error, approve/deny flow, transition error
surfacing, quit, view rendering, snapshot renderer), and gate command
tests (allow/deny/escalate exits, unknown profile fail-closed, corrupt
allowlist fail-closed, ceremony creation, server-down error).
Signed-off-by: Mindburn Labs <mindburnlabs@users.noreply.github.com>
b40ef98 to
8a8e6cf
Compare
Signed-off-by: Mindburn Labs <mindburnlabs@users.noreply.github.com>
|
Closing as superseded/conflicting rather than rebasing. Current |
Summary
Wave 3 / P1-12 of the Rowboat×HELM program — terminal-native ops ergonomics for the kernel CLI. Adapts two mechanisms from the Rowboat deep study (
.tmp-research/findings/cli.md, recommendations 2–3) with original Go implementations and attribution comments. No Rowboat code is copied verbatim.(b) Escalating shell gate —
core/pkg/workstationPorts Rowboat's
extractCommandNames/isBlockedsemantics (apps/cli/src/application/lib/command-executor.ts,config/security.ts) as the parsing front-end of workstation enforcement:ExtractCommandNames— robust to&&,||,|,;,&, backticks,$(...),()subshells,ENV=prefixes, andsudo/env/time/commandwrappers.GateShellCommand— blocked commands become pending approvals, not hard failures, in the dev profile only; production (and any unknown profile) stays fail-closed deny.ShellAllowlistStore— user-editable JSON allowlist (bare array /{allowedCommands}/ truthy map) with mtime+size cache; seeds the minimal Rowboat default set on first use (0600).Fail-closed hardening deviations (documented in-file): recursive wrapper unwrapping,
ENV=/flag skipping after wrappers, corrupt allowlist = error (never silent fallback to defaults), unknown profile ⇒ production.(a)
watchsubcommand —core/cmd/helm-ai-kernelTerminal-native live approval watcher (bubbletea TUI, adapted from Rowboat's Ink
ui.tsx):GET /api/v1/approvals); a failed refresh clears the list and disables actions — fail-closed on API errors.a/dapprove/deny hotkeys →POST /api/v1/approvals/{id}/{approve,deny};rrefresh, arrows/j/knavigate,qquit.--once/--jsonsnapshot modes; automatic snapshot fallback on non-TTY stdout.HELM_ADMIN_API_KEYenv or--api-key-file(0600) — no argv secrets.workstation gateCLIhelm-ai-kernel workstation gate --profile dev|production --command "..."— exit codes0/3/126for allow/pending_approval/deny;--request-approvalturns a dev escalation into a real pending approval ceremony on the server, drainable fromwatch.New dependency
github.com/charmbracelet/bubbletea v1.3.10— the only direct addition; go.mod had no TUI framework. Rendering is plain-text (no lipgloss import) to keep the transitive footprint minimal.Test plan
GOWORK=off go test ./pkg/workstation/ ./cmd/helm-ai-kernel/— greenGOWORK=off go vet— clean;golangci-lint— zero findings in new files (61 pre-existing baseline issues elsewhere untouched)Follow-ups (not in this PR)
/stream-style SSE endpoint sowatchcan stream instead of poll (rec. 2 second half); kernel currently has no run-event bus surface.workstation enforce(currently a separategatesubcommand to avoid changing enforce behavior).watchonce a server-side run listing/stream surface exists.